/*
 * Copyright (c) 2020, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.ohos.camera;

import boofcv.alg.color.ColorFormat;
import boofcv.misc.MovingAverage;
import boofcv.ohos.ConvertBitmap;
import boofcv.ohos.ConvertCameraImage;
import boofcv.ohos.HeLog;
import boofcv.struct.image.ImageBase;
import boofcv.struct.image.ImageType;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.surfaceprovider.SurfaceProvider;
import ohos.agp.graphics.SurfaceOps;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.RectFloat;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.media.image.Image;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import org.ddogleg.struct.DogArray_I8;
import pabeles.concurrency.GrowArray;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * VisualizeCameraSlice
 *
 * @author:Peter Abeles
 * @since 2021-05-11
 */
public abstract class VisualizeCameraSlice extends SimpleCameraSlice {
    protected static final int TIMING_WARM_UP = 3; // number of frames that must be processed before it starts
    private static final String TAG = "VisualizeCameraSlice";
    protected final ReentrantLock bitmapLock = new ReentrantLock();
    protected BitmapMode bitmapMode = BitmapMode.DOUBLE_BUFFER;

    protected PixelMap bitmap;
    protected PixelMap bitmapWork;
    protected DogArray_I8 bitmapTmp = new DogArray_I8();

    protected Matrix imageToView = new Matrix();
    protected Matrix viewToImage = new Matrix();

    protected LinkedBlockingQueue threadQueue;
    protected ThreadPoolExecutor threadPool;
    protected boolean isVisualizeOnlyMostRecent;
    protected volatile long timeOfLastUpdated;

    // number of pixels it searches for when choosing camera resolution
    protected int targetResolution;
    protected final MovingAverage periodConvert = new MovingAverage(0.8); // milliseconds
    protected final Object lockTiming = new Object();
    protected int totalConverted; // counter for frames converted since data type was set

    protected DisplayView displayView;

    private final BoofImage boofImage = new BoofImage();

    /**
     * 构造
     */
    protected VisualizeCameraSlice() {
        final int targetResolutionWidht = 640;
        final int targetResolutionHeight = 480;
        final int keepAliveTime = 50;

        threadQueue = new LinkedBlockingQueue();
        threadPool = new ThreadPoolExecutor(1, 1, keepAliveTime, TimeUnit.MILLISECONDS, threadQueue);
        isVisualizeOnlyMostRecent = true;
        targetResolution = targetResolutionWidht * targetResolutionHeight;
        bitmap = PixelMap.create(ConvertBitmap.createOpts(1, 1, PixelFormat.ARGB_8888));
        bitmapWork = PixelMap.create(ConvertBitmap.createOpts(1, 1, PixelFormat.ARGB_8888));
    }

    /**
     * Data related to converting between Image and BoofCV data types
     * All class data is owned by the lock
     *
     * @author:zhaojun
     * @since 2021-05-11
     */
    protected static class BoofImage {
        protected final Object imageLock = new Object();
        protected ImageType imageType = ImageType.SB_U8;
        protected ColorFormat colorFormat = ColorFormat.RGB;
        /**
         * Images available for use, When inside the stack they must not be referenced anywhere else.
         * When removed they are owned by the thread in which they were removed.
         */
        protected ArrayDeque<ImageBase> stackImages = new ArrayDeque<>();
        protected GrowArray<DogArray_I8> convertWork = new GrowArray<>(DogArray_I8::new);
    }

    /**
     * BitmapMode
     *
     * @author:zhaojun
     * @since 2021-05-11
     */
    public enum BitmapMode {
        /**
         * NONE
         */
        NONE,
        /**
         * UNSAFE
         */
        UNSAFE,
        /**
         * DOUBLE_BUFFER
         */
        DOUBLE_BUFFER;

        BitmapMode() {
        }
    }

    /**
     * Custom view for visualizing results
     *
     * @author:zhaojun
     * @since 2021-05-11
     */
    public class DisplayView extends SurfaceProvider implements SurfaceOps.Callback, Component.DrawTask {
        SurfaceOps mHolder;

        /**
         * 构造
         *
         * @param context
         */
        public DisplayView(Context context) {
            super(context);
            mHolder = getSurfaceOps().get();
            pinToZTop(true); // configure it so that its on top and will be transparent
            mHolder.setFormat(PixelFormat.ARGB_8888.getValue());
            addDrawTask(this); // if this is not set it will not draw
        }

        @Override
        public void surfaceCreated(SurfaceOps surfaceHolder) {
        }

        @Override
        public void surfaceChanged(SurfaceOps surfaceHolder, int i, int i1, int i2) {
        }

        @Override
        public void surfaceDestroyed(SurfaceOps surfaceHolder) {
        }

        @Override
        public void onDraw(Component component, Canvas canvas) {
            onDrawFrame(this, canvas);
        }
    }

    // ------------------------------------自定义方法

    /**
     * startCamera
     *
     * @param layout
     */
    protected void startCamera(ComponentContainer layout) {
        if (isVerbose) {
            HeLog.i(TAG, "startCamera");
        }
        displayView = new DisplayView(getContext());
        startCameraView(layout, displayView);
    }

    /**
     * Same as {@link #setImageType(ImageType, ColorFormat)} but defaults to {@link ColorFormat#RGB RGB}.
     *
     * @param type type of image you wish th convert the into to
     */
    protected void setImageType(ImageType type) {
        this.setImageType(type, ColorFormat.RGB);
    }

    /**
     * Changes the type of image the camera frame is converted to
     *
     * @param type
     * @param colorFormat
     */
    protected void setImageType(ImageType type, ColorFormat colorFormat) {
        if (isVerbose) {
            HeLog.i(TAG, "setImageType type=" + type.toString() + " colorFormat=" + colorFormat);
        }
        synchronized (boofImage.imageLock) {
            boofImage.colorFormat = colorFormat;
            if (!boofImage.imageType.isSameType(type)) {
                boofImage.imageType = type;
                boofImage.stackImages.clear();
            }

            synchronized (lockTiming) {
                totalConverted = 0;
                periodConvert.reset();
            }
        }
    }

    /**
     * processFrame
     *
     * @param image
     * @param width
     * @param height
     */
    @Override
    protected void processFrame(Image image, int width, int height) {
        if (isVerbose) {
            HeLog.i(TAG, "processFrame width=" + width + " height=" + height);
        }
        if (image == null) {
            HeLog.d("processFrame image is null");
            return;
        }

        ImageBase converted;
        /*
        When the image is removed from the stack it's no longer controlled by this class
         */
        synchronized (boofImage.imageLock) {
            if (boofImage.stackImages.isEmpty()) {
                converted = boofImage.imageType.createImage(1, 1);
            } else {
                converted = boofImage.stackImages.pop();
            }
        }
        /*
        below is an expensive operation so care is taken to do it safely outside of locks
        We are now safe to modify the image. I don't believe this function can be invoked multiple times at once
        so the convert work space should be safe from modifications
         */
        long before = System.nanoTime();
        ConvertCameraImage.imageToBoof(image, width, height, boofImage.colorFormat, converted, boofImage.convertWork);
        long after = System.nanoTime();

        // record how long it took to convert the image for diagnostic reasons
        synchronized (lockTiming) {
            totalConverted++;
            if (totalConverted >= TIMING_WARM_UP) {
                final double maxN = 1e-6;
                periodConvert.update((after - before) * maxN);
            }
        }
        threadPool.execute(() -> processImageOuter(converted));
    }

    /**
     * onCameraResolutionChange
     *
     * @param cameraWidth
     * @param cameraHeight
     * @param sensorOrientation
     */
    @Override
    protected void onCameraResolutionChange(int cameraWidth, int cameraHeight, int sensorOrientation) {
        if (isVerbose) {
            HeLog.i(TAG, "onCameraResolutionChange cameraWidth=" + cameraWidth + " cameraHeight=" + cameraHeight
                    + " sensorOrientation" + sensorOrientation);
        }

        // pre-declare bitmap image used for display
        if (bitmapMode != BitmapMode.NONE) {
            bitmapLock.lock();
            try {
                if (bitmap.getImageInfo().size.width != cameraWidth
                        || bitmap.getImageInfo().size.height != cameraHeight) {
                    bitmap = PixelMap.create(ConvertBitmap.createOpts(cameraWidth, cameraHeight,
                            PixelFormat.ARGB_8888));
                    if (bitmapMode == BitmapMode.DOUBLE_BUFFER) {
                        bitmapWork = PixelMap.create(ConvertBitmap.createOpts(cameraWidth, cameraHeight,
                                PixelFormat.ARGB_8888));
                    }
                }
            } finally {
                bitmapLock.unlock();
            }
        }

        DisplayManager displayManager = DisplayManager.getInstance();
        Optional<Display> display = displayManager.getDefaultDisplay(this);
        int rotation = 0;
        if (display.isPresent()) {
            rotation = display.get().getRotation();
        }

        final int ro = 90;
        videoToDisplayMatrix(new Size(cameraWidth, cameraHeight), new Size(viewWidth, viewHeight),
                sensorOrientation, rotation * ro, false);
        if (!imageToView.invert(viewToImage)) {
            throw new RuntimeException("WTF can't invert the matrix?");
        }
    }

    /**
     * 设置视频显示矩阵
     *
     * @param cameraSize
     * @param displaySize
     * @param cameraRotation
     * @param displayRotation
     * @param isStretchToFill
     */
    protected void videoToDisplayMatrix(Size cameraSize, Size displaySize,
                                        int cameraRotation, int displayRotation,
                                        boolean isStretchToFill) {
        if (isVerbose) {
            HeLog.i(TAG, "videoToDisplayMatrix cameraSize=" + cameraSize.toString()
                    + " displaySize=" + displaySize.toString()
                    + " cameraRotation" + cameraRotation + " displayRotation" + displayRotation
                    + " isStretchToFill" + isStretchToFill + " imageToView" + imageToView.toString());
        }
        if (imageToView == null) {
            HeLog.d("videoToDisplayMatrix imageToView is null");
            return;
        }

        // Compute transform from bitmap to view coordinates
        int rotatedWidth = cameraSize.width;
        int rotatedHeight = cameraSize.height;

        int offsetX = 0;
        int offsetY = 0;

        final int rot = 180;
        final int half = 2;
        boolean needToRotateView = (displayRotation == 0 || displayRotation == rot)
                != (cameraRotation == 0 || cameraRotation == rot);

        if (needToRotateView) {
            rotatedWidth = cameraSize.height;
            rotatedHeight = cameraSize.width;
            offsetX = (rotatedWidth - rotatedHeight) / half;
            offsetY = (rotatedHeight - rotatedWidth) / half;
        }

        imageToView.reset();
        float scale = Math.min(
                (float) displaySize.width / rotatedWidth,
                (float) displaySize.height / rotatedHeight);
        if (scale == 0) {
            HeLog.d("displayView has zero width and height");
            return;
        }

        imageToView.postRotate(-displayRotation + cameraRotation, cameraSize.width / (float) half, cameraSize.height / (float) half);
        imageToView.postTranslate(offsetX, offsetY);
        imageToView.postScale(scale, scale);
        if (isStretchToFill) {
            imageToView.postScale(
                    displaySize.width / (rotatedWidth * scale),
                    displaySize.height / (rotatedHeight * scale));
        } else {
            imageToView.postTranslate(
                    (displaySize.width - rotatedWidth * scale) / half,
                    (displaySize.height - rotatedHeight * scale) / half);
        }
    }

    /**
     * Where all the image processing happens. If the number of threads is greater than one then
     * this function can be called multiple times before previous calls have finished.
     * <p>
     * WARNING: If the image type can change this must be before processing it.
     *
     * @param image The image which is to be processed. The image is owned by this function until
     * it returns. After that the image and all it's data will be recycled. DO NOT
     * SAVE A REFERENCE TO IT.
     */
    protected abstract void processImage(ImageBase image);

    /**
     * Specifies the number of threads in the thread-pool. If this is set to an value greater than
     * one you better know how to write concurrent code or else you're going to have an bad time.
     *
     * @param threads
     * @throws IllegalArgumentException
     */
    public void setThreadPoolSize(int threads) {
        if (isVerbose) {
            HeLog.i(TAG, "setThreadPoolSize threads=" + threads);
        }
        if (threads <= 0) {
            throw new IllegalArgumentException("Number of threads must be greater than 0");
        }

        threadPool.setCorePoolSize(threads);
        threadPool.setMaximumPoolSize(threads);
    }

    /**
     * Internal function which manages images and invokes {@link #processImage}.
     *
     * @param image
     */
    private void processImageOuter(ImageBase image) {
        if (isVerbose) {
            HeLog.i(TAG, "processImageOuter");
        }
        long startTime = System.currentTimeMillis();

        // this image is owned by only this process and no other. So no need to lock it while
        // processing
        processImage(image);

        // If an old image finished being processes after an more recent one it won't be visualized
        if (!isVisualizeOnlyMostRecent || startTime > timeOfLastUpdated) {
            timeOfLastUpdated = startTime;

            // Copy this frame
            renderBitmapImage(bitmapMode, image);

            // Update the visualization
            if (displayView != null) {
                getMainTaskDispatcher().asyncDispatch(() -> displayView.invalidate());
            }
        }

        // Put the image into the stack if the image type has not changed
        synchronized (boofImage.imageLock) {
            if (boofImage.imageType.isSameType(image.getImageType())) {
                boofImage.stackImages.add(image);
            }
        }
    }

    /**
     * Renders the bitmap image to output. If you don't wish to have this behavior then override this function.
     * Jsut make sure you respect the bitmap mode or the image might not render as desired or you could crash the app.
     *
     * @param mode
     * @param image
     */
    protected void renderBitmapImage(BitmapMode mode, ImageBase image) {
        if (isVerbose) {
            HeLog.i(TAG, "renderBitmapImage mode=" + mode);
        }
        if (image == null) {
            HeLog.d("renderBitmapImage image is null");
            return;
        }
        switch (mode) {
            case UNSAFE:
                if (image.getWidth() == bitmap.getImageInfo().size.width
                        && image.getHeight() == bitmap.getImageInfo().size.height) {
                    ConvertBitmap.boofToBitmap(image, bitmap, bitmapTmp);
                }
                break;
            case DOUBLE_BUFFER:
                // if there are multiple processing threads bad stuff will happen here. Need one work buffer
                // per thread
                // convert the image. this can be an slow operation
                if (image.getWidth() == bitmapWork.getImageInfo().size.width
                        && image.getHeight() == bitmapWork.getImageInfo().size.height) {
                    ConvertBitmap.boofToBitmap(image, bitmapWork, bitmapTmp);
                }

                // swap the two buffers. If it's locked don't swap. This will skip an frame but will not cause
                // an significant slow down
                if (bitmapLock.tryLock()) {
                    try {
                        PixelMap tmp = bitmapWork;
                        bitmapWork = bitmap;
                        bitmap = tmp;
                    } finally {
                        bitmapLock.unlock();
                    }
                }
                break;
            default:
                break;
        }
    }

    /**
     * Selects an resolution which has the number of pixels closest to the requested value
     *
     * @param widthTexture Width of the texture the preview is displayed inside of.<=0 if no view
     * @param heightTexture Height of the texture the preview is displayed inside of.<=0 if no view
     * @param resolutions array of possible resolutions
     * @return int
     */
    @Override
    protected int selectResolution(int widthTexture, int heightTexture, List<Size> resolutions) {
        if (isVerbose) {
            HeLog.i(TAG, "selectResolution widthTexture=" + widthTexture + " heightTexture" + heightTexture);
        }
        if (resolutions == null) {
            HeLog.d("selectResolution resolutions is null");
            return 0;
        }

        // just wanted to make sure this has been cleaned up
        timeOfLastUpdated = 0;

        // select the resolution here
        final int initBestIndex = -1;
        final double maxNum = 1e-8;
        int bestIndex = initBestIndex;
        double bestAspect = Double.MAX_VALUE;
        double bestArea = 0;

        for (int ii = 0; ii < resolutions.size(); ii++) {
            int width = resolutions.get(ii).width;
            int height = resolutions.get(ii).height;

            double aspectScore = Math.abs(width * height - targetResolution);

            if (aspectScore < bestAspect) {
                bestIndex = ii;
                bestAspect = aspectScore;
                bestArea = width * height;
            } else if (Math.abs(aspectScore - bestArea) <= maxNum) {
                bestIndex = ii;
                double area = width * height;
                if (area > bestArea) {
                    bestArea = area;
                }
            }
        }

        return bestIndex;
    }

    /**
     * Renders the visualizations. Override and invoke super to add your own
     *
     * @param view
     * @param canvas
     */
    protected void onDrawFrame(SurfaceProvider view, Canvas canvas) {
        if (isVerbose) {
            HeLog.i(TAG, "onDrawFrame");
        }
        if (view == null || canvas == null) {
            HeLog.d("onDrawFrame view=" + view + " canvas=" + canvas);
            return;
        }

        // Code below is usefull when debugging display issues
        int widthRe = 0;
        int heightRe = 0;
        if (cameraOpen.mCameraSize != null) {
            widthRe = cameraOpen.mCameraSize.width;
            heightRe = cameraOpen.mCameraSize.height;
        }
        canvasFitCamera(canvas);
        switch (bitmapMode) {
            case UNSAFE:
                canvas.drawPixelMapHolderRect(new PixelMapHolder(this.bitmap),
                        new RectFloat(0, 0, widthRe, heightRe), new Paint());
                break;
            case DOUBLE_BUFFER:
                bitmapLock.lock();
                try {
                    canvas.drawPixelMapHolderRect(new PixelMapHolder(this.bitmap),
                            new RectFloat(0, 0, widthRe, heightRe), new Paint());
                } finally {
                    bitmapLock.unlock();
                }
                break;
            default:
                break;
        }
    }

    /**
     * 恢复原始canvas
     *
     * @param canvas
     */
    protected void resetCanvas(Canvas canvas) {
        if (isVerbose) {
            HeLog.i(TAG, "resetCanvas");
        }
        Matrix matrix = new Matrix();
        if (imageToView.invert(matrix)) {
            canvas.concat(matrix);
        }
    }

    /**
     * canvas设置矩阵
     *
     * @param canvas
     */
    protected void canvasFitCamera(Canvas canvas) {
        if (isVerbose) {
            HeLog.i(TAG, "canvasFitCamera");
        }
        canvas.setMatrix(imageToView);
    }
}
